using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.DirectoryServices.Protocols;
using InfiniTec.Data;
using InfiniTec.DirectoryServices.Properties;
using InfiniTec.Expressions;
using InfiniTec.Threading;
using AsyncOperation=InfiniTec.Threading.AsyncOperation;
using Exp = InfiniTec.Expressions.Expression;

namespace InfiniTec.DirectoryServices
{
    /// <summary>
    /// This class is a decorator for the <see cref="Searcher"/> class which can be used
    /// to search for ActiveDirectory principals: Users, groups and contacts.
    /// </summary>
    public class PrincipalResolver
    {
        private readonly Connection _Connection;
        private readonly AsyncOperationTracker _OperationTracker = new AsyncOperationTracker();
        private readonly List<IPropertyDefinition> _PropertiesToLoad;
        private readonly SearchResultCollection _SearchResult;
        private readonly List<SortKey> _SortKeys;
        private string _Filter;
        private FilterType _FilterType;
        private ResolveTypes _ResolveTypes;
        private SearchMode _SearchMode;
        private SearchToken _SearchToken;

        /// <summary>
        /// Initializes a new instance of the <see cref="PrincipalResolver"/> class.
        /// </summary>
        /// <param name="connection">The connection to use.</param>
        public PrincipalResolver(Connection connection)
        : this((string) null, connection)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PrincipalResolver"/> class.
        /// Use this overload to continue a previous search operation.
        /// </summary>
        /// <param name="connection">The connection to use.</param>
        /// <param name="searchToken">A search token of a previous search operation</param>
        public PrincipalResolver(SearchToken searchToken, Connection connection)
        : this((string) null, connection)
        {
            _SearchToken = searchToken;
            SearchMode = SearchMode.FindPage;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PrincipalResolver"/> class.
        /// </summary>
        /// <param name="searchRoot">The search root.</param>
        /// <param name="connection">The connection to use.</param>
        public PrincipalResolver(string searchRoot, Connection connection)
        {
            ResolveScope = ResolveScope.Forest;
            _Connection = connection;

            SearchRoot = searchRoot;
            _PropertiesToLoad = new List<IPropertyDefinition>();
            _SortKeys = new List<SortKey>();
            _SearchResult = new SearchResultCollection();
        }

        /// <summary>
        /// Gets or sets the resolve types.
        /// </summary>
        /// <value>A valid combination of the <see cref="ResolveTypes"/> enumeration.</value>
        public ResolveTypes ResolveTypes
        {
            get { return _ResolveTypes; }
            set { _ResolveTypes = value; }
        }

        /// <summary>
        /// Gets the search token associated with this instance.
        /// </summary>
        /// <value>The search token.</value>
        public SearchToken SearchToken
        {
            get { return _SearchToken; }
        }

        /// <summary>
        /// Gets or sets the type of the filter.
        /// </summary>
        /// <value>A valid member of the <see cref="FilterType"/> enumeration.</value>
        public FilterType FilterType
        {
            get { return _FilterType; }
            set { _FilterType = value; }
        }

        /// <summary>
        /// Gets or sets the name to search for.
        /// </summary>
        /// <value>The name.</value>
        public string Filter
        {
            get { return _Filter; }
            set { _Filter = value; }
        }

        /// <summary>
        /// Gets or sets the search mode.
        /// </summary>
        /// <value>A valid member of the <see cref="SearchMode"/> enumeration.</value>
        public SearchMode SearchMode
        {
            get { return _SearchMode; }
            set { _SearchMode = value; }
        }

        /// <summary>
        /// Gets or sets the resolve scope.
        /// </summary>
        /// <value>The resolve scope.</value>
        public ResolveScope ResolveScope { get; set; }

        /// <summary>
        /// Gets the sort keys.
        /// </summary>
        /// <value>The sort keys.</value>
        public IList<SortKey> SortKeys
        {
            get { return _SortKeys; }
        }

        /// <summary>
        /// Gets or sets the size of the page.
        /// </summary>
        /// <value>The size of the page.</value>
        public int PageSize { get; set; }

        /// <summary>
        /// Gets or sets the search root.
        /// </summary>
        /// <value>The search root.</value>
        public string SearchRoot { get; set; }

        /// <summary>
        /// Gets the connection to use.
        /// </summary>
        /// <value>The connection to use.</value>
        public Connection Connection
        {
            get { return _Connection; }
        }

        /// <summary>
        /// Gets the properties to load.
        /// </summary>
        /// <value>The properties to load.</value>
        public IList<IPropertyDefinition> PropertiesToLoad
        {
            get { return _PropertiesToLoad; }
        }

        /// <summary>
        /// Gets or sets the size limit.
        /// </summary>
        /// <remarks>
        /// If more entries are found than specified by this value, an exception is thrown.
        /// </remarks>
        /// <value>The size limit.</value>
        public int SizeLimit { get; set; }

        /// <summary>
        /// Gets the search result.
        /// </summary>
        /// <value>The search result.</value>
        public IList<ActiveDirectoryPrincipal> SearchResult
        {
            get { return _SearchResult; }
        }

        /// <summary>
        /// This event is invoked when the the search operation has completed.
        /// </summary>
        public event EventHandler<AsyncCompletedEventArgs> FindCompleted;

        /// <summary>
        /// Calls the <see cref="FindCompleted"/> event.
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnFindCompleted(AsyncCompletedEventArgs e)
        {
            if (FindCompleted != null) FindCompleted(this, e);
        }

        /// <summary>
        /// Synchronously starts the search operation.
        /// </summary>
        /// <returns></returns>
        public void Find()
        {
            AsyncOperation<IList<Item>> op = GetFindOperation();
            op.RunSynchronously();
        }

        /// <summary>
        /// Asynchronously starts the search operation.
        /// </summary>
        public void FindAsync()
        {
            AsyncOperation<IList<Item>> operation;

            operation = GetFindOperation();

            operation.Completed += SearchCompleted;

            operation.Run();
        }

        private void SearchCompleted(object sender, AsyncCompletedEventArgs e)
        {
            OnFindCompleted(e);
        }

        /// <summary>
        /// Returns the search operation used to perform the search based
        /// ojn the specified settings.
        /// </summary>
        /// <returns></returns>
        public virtual AsyncOperation<IList<Item>> GetFindOperation()
        {
            return new AsyncOperation<IList<Item>>("Find", FindOperationInternal(), _OperationTracker, false, null);
        }

        private IEnumerable<OperationAction> FindOperationInternal()
        {
            Searcher searcher;
            ActiveDirectoryPrincipal entry;

            if (_SearchMode == SearchMode.FindPage && _SearchToken != null)
            {
                searcher = new Searcher(Connection, _SearchToken);
            }
            else
            {
                if (_ResolveTypes == ResolveTypes.None)
                    throw new ArgumentException("At least one type of principals must be searched.");

                searcher = new Searcher(SearchRoot, Connection, _PropertiesToLoad.ToArray());
                searcher.Scope = SearchScope.Subtree;
                searcher.IncludeDeletedItems = false;
                searcher.PageSize = PageSize;

                foreach (IPropertyDefinition property in GetPropertiesToLoad())
                {
                    searcher.PropertiesToLoad.Add(property);
                }

                switch (ResolveScope)
                {
                    case ResolveScope.Domain:
                        searcher.NamingContextScope = NamingContextScope.Domain;
                        break;
                    case ResolveScope.Forest:
                        searcher.NamingContextScope = NamingContextScope.IncludeSubDomains;
                        break;
                    default:
                        throw new ArgumentException("The specified resolve scope is invalid.", "ResolveScope");
                }

                searcher.SizeLimit = SizeLimit;
                foreach (SortKey key in SortKeys)
                {
                    searcher.SortKeys.Add(key);
                }

                _SearchResult.Clear();

                switch (_FilterType)
                {
                    case FilterType.Name:
                    case FilterType.SimpleLdapFilter:
                        searcher.Constraints = CreateLdapFilter();
                        break;
                    case FilterType.RawLdapFilter:
                        searcher.Constraints = _Filter;
                        break;
                    default:
                        throw new InvalidOperationException(Resources.InvalidFilterTypeException);
                }
            }

            AsyncOperation operation = searcher.GetFindOperation(_SearchMode, false);
            yield return new NestedOperation(operation);

            if (SearchMode == SearchMode.FindPage)
            {
                _SearchToken = searcher.SearchToken;
            }

            foreach (Item item in searcher.SearchResult)
            {
                entry = ParseSearchResultItem(item);
                if (_SearchResult.Contains(entry)) continue;
                _SearchResult.Add(entry);
            }

            searcher.SearchResult.ToString();
        }

        private IEnumerable<IPropertyDefinition> GetPropertiesToLoad()
        {
            foreach (IPropertyDefinition property in PropertiesToLoad)
            {
                yield return property;
            }
            foreach (IPropertyDefinition property in SchemaMapping.CommonSchema)
            {
                yield return property;
            }

            // Since tokenGroups and other computed groupmemberships cannot be programmatically
            // fetched, they are ommited here.
            yield return WellknownProperties.GroupMembership.MemberOf;

            if ((_ResolveTypes & ResolveTypes.User) == ResolveTypes.User ||
                (_ResolveTypes & ResolveTypes.Contacts) == ResolveTypes.Contacts)
            {
                foreach (IPropertyDefinition property in SchemaMapping.UserSchema)
                {
                    yield return property;
                }
            }
            if ((_ResolveTypes & ResolveTypes.Groups) == ResolveTypes.Groups)
            {
                foreach (IPropertyDefinition property in SchemaMapping.GroupSchema)
                {
                    yield return property;
                }
            }
        }

        private static ActiveDirectoryPrincipal ParseSearchResultItem(Item item)
        {
            var entry = new ActiveDirectoryPrincipal(item);
            if (entry.ObjectClass.Contains(WellknownProperties.ObjectClasses.Group))
            {
                return new ActiveDirectoryGroup(item);
            }
            if (entry.ObjectClass.Contains(WellknownProperties.ObjectClasses.User))
            {
                return new ActiveDirectoryUser(item);
            }
            if (entry.ObjectClass.Contains(WellknownProperties.ObjectClasses.Contact))
            {
                return new ActiveDirectoryContact(item);
            }
            return new ActiveDirectoryPrincipal(item);
        }

        private string CreateLdapFilter()
        {
            if (string.IsNullOrEmpty(_Filter)) throw new ArgumentNullException("Filter");

            switch (FilterType)
            {
                case FilterType.SimpleLdapFilter:
                    return string.Format("(&{0}({1}))",
                                         CreateResolveTypesFilter().ToString(new LdapFormatter()),
                                         _Filter);
                case FilterType.Name:

                    Expression expression = Expression.And(
                    CreateResolveTypesFilter(),
                    Expression.Or(
                    Expression.Equals(WellknownProperties.Special.AmbiguousNameResolution, _Filter + "*"),
                    CreateInitialsExpression(_Filter)));
                    return expression.ToString(new LdapFormatter());
                default:
                    throw new InvalidOperationException(Resources.InvalidFilterTypeException);
            }
        }

        private Exp CreateResolveTypesFilter()
        {
            List<Expression> result;

            result = new List<Expression>(2);
            if ((ResolveTypes & ResolveTypes.Groups) == ResolveTypes.Groups)
                result.Add(Expression.Equals(WellknownProperties.Common.ObjectCategory, WellknownProperties.ObjectCategories.Group));
            if ((ResolveTypes & ResolveTypes.User) == ResolveTypes.User)
                result.Add(Expression.And(
                           Expression.Equals(WellknownProperties.Common.ObjectCategory, WellknownProperties.ObjectCategories.Person),
                           Expression.Equals(WellknownProperties.Common.ObjectClass, WellknownProperties.ObjectClasses.User)));
            if ((ResolveTypes & ResolveTypes.Contacts) == ResolveTypes.Contacts)
                result.Add(Expression.And(
                           Expression.Equals(WellknownProperties.Common.ObjectCategory, WellknownProperties.ObjectCategories.Person),
                           Expression.Equals(WellknownProperties.Common.ObjectClass, WellknownProperties.ObjectClasses.Contact)));

            return (result.Count > 1) ? Expression.Or(result.ToArray()) : result[0];
        }

        private static Exp CreateInitialsExpression(string name)
        {
            if (name.Length == 2)
                return Expression.And(
                Expression.Equals(WellknownProperties.User.FirstName, name[0] + "*"),
                Expression.Equals(WellknownProperties.User.LastName, name[1] + "*"));
            return new NullExpression();
        }
    }
}